package com.graduate.emos.wx.config.shiro;

import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

@Component
@Scope("prototype")//因为该类中要读写ThreadLocal中的数据，所以类必须要设置成例的
public class OAuth2Filter extends AuthenticatingFilter {
    //注入存储令牌的媒介类
    @Autowired
    private ThreadLocalToken threadLocalToken;
    //注入redis令牌缓存的过期天数
    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;
    //注入jwt工具类
    @Autowired
    private JwtUtil jwtUtil;
    //注入redis模板对象
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 拦截请求后用于把令牌字符串封装成令牌对象,令牌对象将会交给Shiro框架使用
     * @param servletRequest  请求
     * @param servletResponse 响应
     * @return 封装好的令牌对象 OAuth2Token
     * @throws Exception 异常
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //获取token
        String token = getRequestToken((HttpServletRequest) servletRequest);
        if (!StrUtil.isBlank(token)){
            //如果获取到的token不为空封装成令牌对象
            return new OAuth2Token(token);
        }
        return null;
    }

    /**
     * 拦截请求判断请求是否需要经过Shiro处理
     * @param servletRequest   请求
     * @param servletResponse  响应
     * @return true:放行 false:处理
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) {
        //向下转型
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //Ajax提交application/json数据的时候会先发出Option请求，如果是options请求则放行
        if (request.getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }
        //其余方法拦截交由Shiro处理
        return false;
    }

    /**
     * 对需要处理的方法进行处理
     * @param servletRequest 请求
     * @param servletResponse 响应
     * @return true:认证或授权成功  false:认证失败
     * @throws Exception 异常
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //向下转型
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //设置响应内容类型
        response.setContentType("text/html");
        //设置编码
        response.setCharacterEncoding("UTF-8");
        //设置支持跨域
        response.setHeader("Access-Control-Allow-Credentials","true");
        response.setHeader("Access-Control-Allow-Credentials",request.getHeader("Origin"));
        //清空令牌媒介类中的数据
        threadLocalToken.clear();

        //从请求中获取token
        String token = getRequestToken(request);
        if (StrUtil.isBlank(token)) {
            //如果token为空
            //设置响应状态---未授权
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            //提示
            response.getWriter().print("未检测到令牌！");
            //不放行
            return false;
        }
        //如果token不为空
        //验证令牌是否有效(内容有效、期限有效)
        try{
            jwtUtil.verifierToken(token);
        }catch (TokenExpiredException e){
            //令牌过期判断Redis中的令牌是否过期
            if (redisTemplate.hasKey(token)){
                //redis缓存中的token还没有过期则对客户端的token进行续期
                //先删除redis中的token缓存
                redisTemplate.delete(token);
                //从token中获取用户id，根据用户id重新生成token
                int userId = jwtUtil.getUserId(token);
                token = jwtUtil.createToken(userId);
                //将新的token存储到redis缓存
                redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
                //将新的token也存到媒介类中
                threadLocalToken.setToken(token);
            }else {
                //redis缓存中的令牌也过期
                //设置响应状态---未授权
                response.setStatus(HttpStatus.SC_UNAUTHORIZED);
                //提示
                response.getWriter().print("令牌已过期！");
                return false;
            }
        }catch (JWTDecodeException e){
            //令牌内容不对
            //设置响应状态---未授权
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            //提示
            response.getWriter().print("令牌无效！");
            return false;
        }
        //令牌有效去Realm类中去执行认证或授权
        //executeLogin()执行登录会到Realm类中去认证
        return executeLogin(request, response);
    }

    /**
     * 当用户登录失败时调用
     * @param token             令牌
     * @param e                 异常
     * @param servletRequest    请求
     * @param servletResponse   响应
     * @return true:放行  false:拦截
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest servletRequest, ServletResponse servletResponse) {
        //向下转型
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //设置响应内容类型
        response.setContentType("text/html");
        //设置编码
        response.setCharacterEncoding("UTF-8");
        //设置支持跨域
        response.setHeader("Access-Control-Allow-Credentials","true");
        response.setHeader("Access-Control-Allow-Credentials",request.getHeader("Origin"));
        //设置响应状态---未授权
        response.setStatus(HttpStatus.SC_UNAUTHORIZED);
        //提示
        try{
            response.getWriter().print(e.getMessage());
        }catch (Exception exception){
            exception.printStackTrace();
        }
        return false;
    }

    /**
     * 获取请求中的token
     * @param request 请求
     * @return token字符串
     */
    private String getRequestToken(HttpServletRequest request){
        //从请求头中获取token
        String token = request.getHeader("token");
        if (StrUtil.isBlank(token)) {
            //如果从请求头中获取的token为空，则从请求体中获取
            token = request.getParameter("token");
        }
        return token;
    }
}
